--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 35fe2071492914810e609b8428bd99dc73c662d0
Parents : f62b203
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-10T14:42:31-05:00
feat(websocket): attach request to WebSocketResponse for session checks and update authorization logic
Changes
2 files changed, 63 insertions(+), 1 deletions(-)
Diff
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 85f79448..55fc68c5 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -7275,6 +7275,9 @@ class ReticulumMeshChat:
max_msg_size=50 * 1024 * 1024,
)
await websocket_response.prepare(request)
+ # aiohttp WebSocketResponse does not expose .request; keep it for
+ # session checks on authenticated mutators (nomadnet downloads, etc).
+ websocket_response._meshchatx_request = request
# add client to connected clients list
self.websocket_clients.append(websocket_response)
@@ -17784,7 +17787,9 @@ class ReticulumMeshChat:
async def _websocket_session_authorized(self, client) -> bool:
if not self.auth_enabled:
return True
- request = getattr(client, "request", None)
+ request = getattr(client, "_meshchatx_request", None)
+ if request is None:
+ request = getattr(client, "request", None)
if request is None:
return False
try:
diff --git a/tests/backend/test_websocket_config_security.py b/tests/backend/test_websocket_config_security.py
index 3cbba4af..443f2e80 100644
--- a/tests/backend/test_websocket_config_security.py
+++ b/tests/backend/test_websocket_config_security.py
@@ -53,6 +53,63 @@ async def test_websocket_mutator_rejected_without_session_when_auth_enabled(mock
assert '"Authentication required"' in payload
+@pytest.mark.asyncio
+async def test_websocket_session_authorized_uses_attached_request(mock_app):
+ """Aiohttp WebSocketResponse has no .request; mutators must use the attached one."""
+ mock_app.config.auth_enabled.set(True)
+ identity_hash = "ab" * 16
+ mock_app.identity = MagicMock()
+ mock_app.identity.hash.hex.return_value = identity_hash
+
+ client = MagicMock(spec=["send_str", "_meshchatx_request"])
+ client._meshchatx_request = MagicMock()
+ session = {"authenticated": True, "identity_hash": identity_hash}
+
+ with patch("meshchatx.meshchat.get_session", AsyncMock(return_value=session)):
+ assert await mock_app._websocket_session_authorized(client) is True
+
+
+@pytest.mark.asyncio
+async def test_websocket_session_authorized_false_without_attached_request(mock_app):
+ mock_app.config.auth_enabled.set(True)
+ client = MagicMock(spec=["send_str"])
+ assert await mock_app._websocket_session_authorized(client) is False
+
+
+@pytest.mark.asyncio
+async def test_nomadnet_page_download_not_rejected_when_ws_request_attached(mock_app):
+ mock_app.config.auth_enabled.set(True)
+ identity_hash = "cd" * 16
+ mock_app.identity = MagicMock()
+ mock_app.identity.hash.hex.return_value = identity_hash
+
+ client = MagicMock(spec=["send_str", "_meshchatx_request"])
+ client._meshchatx_request = MagicMock()
+ client.send_str = AsyncMock()
+ session = {"authenticated": True, "identity_hash": identity_hash}
+
+ with (
+ patch("meshchatx.meshchat.get_session", AsyncMock(return_value=session)),
+ patch(
+ "meshchatx.meshchat.AsyncUtils.run_async",
+ side_effect=_run_async_immediate,
+ ),
+ ):
+ await mock_app.on_websocket_data_received(
+ client,
+ {
+ "type": "nomadnet.page.download",
+ # Intentionally incomplete payload: auth must pass before handler
+ # returns early for missing nomadnet_page_download.
+ },
+ )
+ await asyncio.sleep(0)
+
+ for call in client.send_str.await_args_list:
+ assert "Authentication required" not in call.args[0]
+ assert "Rejected unauthorized" not in call.args[0]
+
+
@pytest.mark.asyncio
async def test_websocket_read_allowed_without_session_when_auth_enabled(mock_app):
mock_app.config.auth_enabled.set(True)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────